<template>
  <div
    :class="
      `sw-tree ${
        showDescr ? 'sw-show-descr' : 'sw-hide-descr'
      } sw-format-${displayFormat}`
    "
    :style="{
      'background-color': treeContentBackground,
      position: currentDeep > 1 ? '' : 'relative'
    }"
    @click.stop="handleClick($event)"
  >
    <template v-if="Array.isArray(data) || isObject(data)">
      <!-- Left Closed -->
      <bracket-left
        :visible.sync="visible"
        :data="data"
        :show-length="showLength"
        :not-last-key="notLastKey"
      >
        <span v-if="currentDeep > 1 && !Array.isArray(parentData)"
          >{{ currentKey }}:</span
        >
      </bracket-left>

      <!-- 
        Data content, 
        1. when object, key represents the key name, 
        2. when array,  key represents index 
      -->
      <div
        v-for="(item, key) in data"
        v-show="visible"
        class="sw-tree-content"
        :key="key"
      >
        <json-tree
          :parent-data="data"
          :data="item"
          :deep="deep"
          :show-length="showLength"
          :path="path + (Array.isArray(data) ? `[${key}]` : `.${key}`)"
          :allow-interaction="allowInteraction"
          :current-key="key"
          :display-format="displayFormat"
          :current-deep="currentDeep + 1"
          @click="handleItemClick"
        >
        </json-tree>
      </div>

      <!-- Right closed -->
      <bracket-right
        :visible.sync="visible"
        :data="data"
        :not-last-key="notLastKey"
      >
      </bracket-right>
    </template>

    <content-block
      v-else
      :parentDataType="getDataType(parentData)"
      :dataType="getDataType(data)"
      :text="data + ''"
      :notLastKey="notLastKey"
      :displayFormat="displayFormat"
      :currentKey="currentKey"
    >
    </content-block>
  </div>
</template>

<script>
import ContentBlock from "@/components/tree/ContentBlock";
import BracketLeft from "@/components/tree/BracketLeft";
import BracketRight from "@/components/tree/BracketRight";

export default {
  name: "json-tree",
  components: {
    ContentBlock,
    BracketLeft,
    BracketRight
  },
  props: {
    // Current tree data
    data: {},

    // Defining the depth of the tree, subtrees larger than the depth will not be expanded
    deep: { type: Number, default: Infinity },

    // Whether to display the length of the array | object
    showLength: { type: Boolean, default: false },

    // Data level top level path
    path: { type: String, default: "root" },

    allowInteraction: { type: Boolean, default: true },

    // Parent data of current node
    parentData: {},

    // The depth of the current tree starts with the root node as 0, so the depth of the first layer tree is 1, and the recursion is incremented succ
    currentDeep: { type: Number, default: 1 },

    //  1. when object, currentKey represents the key name,
    //  2. when array,  currentKey represents index
    currentKey: [Number, String],

    //Display Format can be 'json' or 'text'
    displayFormat: { type: String, default: "json" }
  },
  data() {
    return {
      visible: this.currentDeep <= this.deep,
      treeContentBackground: "transparent",
      showDescr: true
    };
  },

  computed: {
    // to determine whether to add ","
    lastKey() {
      if (Array.isArray(this.parentData)) {
        return this.parentData.length - 1;
      } else if (this.isObject(this.parentData)) {
        let arr = Object.keys(this.parentData);
        return arr[arr.length - 1];
      }
    },
    notLastKey() {
      return this.currentKey !== this.lastKey;
    }
  },
  methods: {
    // Trigger component's click event
    handleClick(e) {
      if (this.allowInteraction) {
        this.$emit("click", this.path, this.data);
      }
    },

    // Handles the click event fired by the subtree and passes it to the top
    handleItemClick(path, data) {
      this.$emit("click", path, data);
    },

    isObject(value) {
      return this.getDataType(value) === "object";
    },

    getDataType(value) {
      return Object.prototype.toString
        .call(value)
        .slice(8, -1)
        .toLowerCase();
    }
  },
  watch: {
    deep(newVal) {
      this.visible = this.currentDeep <= newVal;
    }
  }
};
</script>

<style lang="scss">
@import "~@/assets/styles/_vars.scss";

.sw-tree {
  font-family: $sw-font-mono;
  font-size: 12px;
  .sw-tree-content {
    padding-left: 1em;
    border-left: 1px dotted #ccc;
  }

  .sw-tree-bracket-left,
  .sw-tree-bracket-right {
    cursor: pointer;
    border: 1px solid transparent;
    border-radius: 3px;
    padding: 0 20px 2px 0;
    &:hover {
      color: $sw-primary-color;
      background: #efefef;
      border: 1px solid #ccc;
    }
  }

  .sw-datatype {
    &-null {
      color: $sw-soft-red;
    }
    &-bool,
    &-numb,
    &-inte,
    &-number,
    &-boolean {
      color: $sw-info;
    }
    &-stri,
    &-string {
      color: darken($sw-green, 5%);
    }
    &-enum {
      color: $sw-orange;
    }
  }

  .sw-content-block {
    .sw-content-key {
      white-space: nowrap;
    }
    .sw-content {
      white-space: nowrap;
    }
    .sw-content.sw-datatype-enum {
      white-space: normal;
      padding-top: 0;
      min-width: 150px;
    }

    display: flex;
    align-items: stretch;
    width: 100%;
    &:hover {
      background: #efefef;
    }
  }

  .sw-descr {
    min-width: 100px;
    white-space: nowrap;
    text-overflow: ellipsis;
    width: 100%;
    display: inline-block;
    overflow: hidden;
    color: $sw-gray-text;
    text-align: right;
    &.sw-descr-collapsed {
      white-space: nowrap;
    }
    &.sw-descr-expanded {
      white-space: normal;
    }
  }
  .sw-show-descr .sw-descr {
    display: block;
  }
  .sw-hide-descr .sw-descr {
    display: none;
  }
  .sw-descr-expanded + .sw-descr-expander {
    border-left: 2px solid #ccc;
  }
  .sw-descr-expander {
    padding: 0 5px;
    margin-left: 5px;
    cursor: pointer;
    color: orange;
    &:hover {
      color: orangered;
    }
  }
}
</style>

